// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (C) 2016-2018, LomboTech Co.Ltd.
 * Authors:
 *	lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute  it and/or modify it
 * under  the terms of  the GNU General  Public License as published by the
 * Free Software Foundation;  either version 2 of the  License, or (at your
 * option) any later version.
 */

#include <linux/device.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/pm_runtime.h>
#include <linux/slab.h>

#include "lombo_vo_manage.h"

static int lombo_vo_device_match(struct device *dev, struct device_driver *drv)
{
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);

	/* attempt OF style match */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* compare VO device and driver names */
	if (!strcmp(vo->name, drv->name))
		return 1;

	return 0;
}

static int lombo_vo_uevent(struct device *dev, struct kobj_uevent_env *env)
{
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);
	int err;

	err = of_device_uevent_modalias(dev, env);
	if (err != -ENODEV)
		return err;

	add_uevent_var(env, "MODALIAS=%s%s", LOMBO_VO_MODULE_PREFIX,
		       vo->name);

	return 0;
}

static const struct dev_pm_ops lombo_vo_device_pm_ops = {
	.runtime_suspend = pm_generic_runtime_suspend,
	.runtime_resume = pm_generic_runtime_resume,
	.suspend = pm_generic_suspend,
	.resume = pm_generic_resume,
	.freeze = pm_generic_freeze,
	.thaw = pm_generic_thaw,
	.poweroff = pm_generic_poweroff,
	.restore = pm_generic_restore,
};

static struct bus_type lombo_vo_bus_type = {
	.name = "lombo-vo",
	.match = lombo_vo_device_match,
	.uevent = lombo_vo_uevent,
	.pm = &lombo_vo_device_pm_ops,
};

static int of_device_match(struct device *dev, void *data)
{
	return dev->of_node == data;
}

/**
 * of_find_lombo_vo_device_by_node() - find the LOMBO VO device matching a
 *    device tree node
 * @np: device tree node
 *
 * Return: A pointer to the LOMBO VO device corresponding to @np or NULL if no
 *    such device exists (or has not been registered yet).
 */
struct lombo_vo_device *of_find_lombo_vo_device_by_node(
		struct device_node *np)
{
	struct device *dev;

	dev = bus_find_device(&lombo_vo_bus_type, NULL, np, of_device_match);

	return dev ? to_lombo_vo_device(dev) : NULL;
}
EXPORT_SYMBOL(of_find_lombo_vo_device_by_node);

static void lombo_vo_dev_release(struct device *dev)
{
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);

	of_node_put(dev->of_node);
	kfree(vo);
}

static const struct device_type lombo_vo_device_type = {
	.release = lombo_vo_dev_release,
};

static struct lombo_vo_device *lombo_vo_device_alloc(
		struct lombo_vo_host *host)
{
	struct lombo_vo_device *vo;

	vo = kzalloc(sizeof(*vo), GFP_KERNEL);
	if (!vo)
		return ERR_PTR(-ENOMEM);

	vo->host = host;
	vo->dev.bus = &lombo_vo_bus_type;
	vo->dev.parent = host->dev;
	vo->dev.type = &lombo_vo_device_type;

	device_initialize(&vo->dev);

	return vo;
}

static int lombo_vo_device_add(struct lombo_vo_device *vo)
{
	struct lombo_vo_host *host = vo->host;

	dev_set_name(&vo->dev, "%s.%d", dev_name(host->dev),  vo->channel);

	return device_add(&vo->dev);
}

#if IS_ENABLED(CONFIG_OF)
static struct lombo_vo_device *
of_lombo_vo_device_add(struct lombo_vo_host *host, struct device_node *node)
{
	struct device *dev = host->dev;
	struct lombo_vo_device_info info = { };
	int ret;
	u32 reg;

	if (of_modalias_node(node, info.type, sizeof(info.type)) < 0) {
		dev_err(dev, "modalias failure on %pOF\n", node);
		return ERR_PTR(-EINVAL);
	}

	ret = of_property_read_u32(node, "reg", &reg);
	if (ret) {
		dev_err(dev, "device node %pOF has no valid reg property: %d\n",
			node, ret);
		return ERR_PTR(-EINVAL);
	}

	info.channel = reg;
	info.node = of_node_get(node);

	return lombo_vo_device_register_full(host, &info);
}
#else
static struct lombo_vo_device *
of_lombo_vo_device_add(struct lombo_vo_host *host, struct device_node *node)
{
	return ERR_PTR(-ENODEV);
}
#endif

/**
 * lombo_vo_device_register_full - create a LOMBO VO device
 * @host: LOMBO VO host to which this device is connected
 * @info: pointer to template containing LOMBO VO device information
 *
 * Create a LOMBO VO device by using the device information provided by
 * lombo_vo_device_info template
 *
 * Returns:
 * A pointer to the newly created LOMBO VO device, or, a pointer encoded
 * with an error
 */
struct lombo_vo_device *
lombo_vo_device_register_full(struct lombo_vo_host *host,
			      const struct lombo_vo_device_info *info)
{
	struct lombo_vo_device *vo;
	struct device *dev = host->dev;
	int ret;

	if (!info) {
		dev_err(dev, "invalid lombo_vo_device_info pointer\n");
		return ERR_PTR(-EINVAL);
	}

	vo = lombo_vo_device_alloc(host);
	if (IS_ERR(vo)) {
		dev_err(dev, "failed to allocate LOMBO VO device %ld\n",
			PTR_ERR(vo));
		return vo;
	}

	vo->dev.of_node = info->node;
	vo->channel = info->channel;
	strlcpy(vo->name, info->type, sizeof(vo->name));

	ret = lombo_vo_device_add(vo);
	if (ret) {
		dev_err(dev, "failed to add LOMBO VO device %d\n", ret);
		kfree(vo);
		return ERR_PTR(ret);
	}

	return vo;
}
EXPORT_SYMBOL(lombo_vo_device_register_full);

/**
 * lombo_vo_device_unregister - unregister LOMBO VO device
 * @dsi: LOMBO VO peripheral device
 */
void lombo_vo_device_unregister(struct lombo_vo_device *vo)
{
	device_unregister(&vo->dev);
}
EXPORT_SYMBOL(lombo_vo_device_unregister);

static DEFINE_MUTEX(vo_host_lock);
static LIST_HEAD(vo_host_list);

/**
 * of_find_lombo_vo_host_by_node() - find the LOMBO VO host matching a
 *				     device tree node
 * @node: device tree node
 *
 * Returns:
 * A pointer to the LOMBO VO host corresponding to @node or NULL if no
 * such device exists (or has not been registered yet).
 */
struct lombo_vo_host *of_find_lombo_vo_host_by_node(struct device_node *node)
{
	struct lombo_vo_host *host;

	mutex_lock(&vo_host_lock);

	list_for_each_entry(host, &vo_host_list, list) {
		if (host->dev->of_node == node) {
			mutex_unlock(&vo_host_lock);
			return host;
		}
	}

	mutex_unlock(&vo_host_lock);

	return NULL;
}
EXPORT_SYMBOL(of_find_lombo_vo_host_by_node);

int lombo_vo_host_register(struct lombo_vo_host *host)
{
	struct device_node *node;

	for_each_available_child_of_node(host->dev->of_node, node) {
		/* skip nodes without reg property */
		if (!of_find_property(node, "reg", NULL))
			continue;
		of_lombo_vo_device_add(host, node);
	}

	mutex_lock(&vo_host_lock);
	list_add_tail(&host->list, &vo_host_list);
	mutex_unlock(&vo_host_lock);

	return 0;
}
EXPORT_SYMBOL(lombo_vo_host_register);

static int lombo_vo_remove_device_fn(struct device *dev, void *priv)
{
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);

	lombo_vo_device_unregister(vo);

	return 0;
}

void lombo_vo_host_unregister(struct lombo_vo_host *host)
{
	device_for_each_child(host->dev, NULL, lombo_vo_remove_device_fn);

	mutex_lock(&vo_host_lock);
	list_del_init(&host->list);
	mutex_unlock(&vo_host_lock);
}
EXPORT_SYMBOL(lombo_vo_host_unregister);

/**
 * lombo_vo_attach - attach a LOMBO VO device to its VO host
 * @dsi: LOMBO VO peripheral
 */
int lombo_vo_attach(struct lombo_vo_device *vo)
{
	const struct lombo_vo_host_ops *ops = vo->host->ops;

	if (!ops || !ops->attach)
		return -EINVAL;

	return ops->attach(vo->host, vo);
}
EXPORT_SYMBOL(lombo_vo_attach);

/**
 * lombo_vo_detach - detach a LOMBO VO device from its VO host
 * @dsi: LOMBO VO peripheral
 */
int lombo_vo_detach(struct lombo_vo_device *vo)
{
	const struct lombo_vo_host_ops *ops = vo->host->ops;

	if (!ops || !ops->detach)
		return -EINVAL;

	return ops->detach(vo->host, vo);
}
EXPORT_SYMBOL(lombo_vo_detach);

static ssize_t lombo_vo_device_transfer(struct lombo_vo_device *vo,
					struct lombo_vo_msg *msg)
{
	const struct lombo_vo_host_ops *ops = vo->host->ops;

	if (!ops || !ops->transfer)
		return -EINVAL;

	return ops->transfer(vo->host, msg);
}

ssize_t lombo_vo_write_buffer(struct lombo_vo_device *vo,
					void *data, size_t len)
{
	struct lombo_vo_msg msg = {
		.tx_buf = data,
		.tx_len = len
	};

	if (!vo || !data)
		return -EINVAL;

	switch (len) {
	case 0:
		return -EINVAL;

	case 1:
		msg.type = LOMBO_CPU_IF_MSG_WR_IDX;
		break;

	case 2:
		msg.type = LOMBO_CPU_IF_MSG_WR_REG;
		break;

	default:
		msg.type = LOMBO_CPU_IF_MSG_WR_REG;
		break;
	}

	return lombo_vo_device_transfer(vo, &msg);
}
EXPORT_SYMBOL(lombo_vo_write_buffer);

static int lombo_vo_drv_probe(struct device *dev)
{
	struct lombo_vo_driver *drv = to_lombo_vo_driver(dev->driver);
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);

	return drv->probe(vo);
}

static int lombo_vo_drv_remove(struct device *dev)
{
	struct lombo_vo_driver *drv = to_lombo_vo_driver(dev->driver);
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);

	return drv->remove(vo);
}

static void lombo_vo_drv_shutdown(struct device *dev)
{
	struct lombo_vo_driver *drv = to_lombo_vo_driver(dev->driver);
	struct lombo_vo_device *vo = to_lombo_vo_device(dev);

	drv->shutdown(vo);
}

/**
 * lombo_vo_driver_register_full() - register a driver for LOMBO VO devices
 * @drv: LOMBO VO driver structure
 * @owner: owner module
 *
 * Return: 0 on success or a negative error code on failure.
 */
int lombo_vo_driver_register_full(struct lombo_vo_driver *drv,
				  struct module *owner)
{
	drv->driver.bus = &lombo_vo_bus_type;
	drv->driver.owner = owner;

	if (drv->probe)
		drv->driver.probe = lombo_vo_drv_probe;
	if (drv->remove)
		drv->driver.remove = lombo_vo_drv_remove;
	if (drv->shutdown)
		drv->driver.shutdown = lombo_vo_drv_shutdown;

	return driver_register(&drv->driver);
}
EXPORT_SYMBOL(lombo_vo_driver_register_full);

/**
 * lombo_vo_driver_unregister() - unregister a driver for LOMBO VO devices
 * @drv: LOMBO VO driver structure
 *
 * Return: 0 on success or a negative error code on failure.
 */
void lombo_vo_driver_unregister(struct lombo_vo_driver *drv)
{
	driver_unregister(&drv->driver);
}
EXPORT_SYMBOL(lombo_vo_driver_unregister);

static int __init lombo_vo_bus_init(void)
{
	return bus_register(&lombo_vo_bus_type);
}
postcore_initcall(lombo_vo_bus_init);

MODULE_AUTHOR("lomboswer <lomboswer@lombotech.com>");
MODULE_DESCRIPTION("LOMBO VO Bus");
MODULE_LICENSE("GPL and additional rights");
